import socket
import threading
import time
import re

import communication
import data_box

_VERSION = "1.0.2d" #: The version of UTc! with which this server is compatible.
_TEAM_SIZE = 16 #: The maximum number of players that may play on a single team.

_TEAM_REGEXP = re.compile(r"(TEAM) \w+ (\d+)") #: A protocol-regexp that catches team-change events.
_PART_REGEXP = re.compile(r"(PART) \w+") #: A protocol-regexp that catches player-part events.
_PLAY_REGEXP = re.compile(r"(PLAY) \w+ (\d+),(\d+),(\d+) (\d{2}) (\d{2}|__),(\d{2}|__),(\d{2}|__),(\d{2}|__) (\d),(\d),(\d),(\d) (\d{2}|__),(\d{2}|__),(\d{2}|__) (\d)") #: A protocol-regexp that catches player-game-join events.
_TEXT_REGEXP = re.compile(r"TEXT \w+ .+") #: A protocol-regexp that catches player-text events.
_REGEXP_HANDLING = (
 (_TEAM_REGEXP, True),
 (_PART_REGEXP, True),
 (_PLAY_REGEXP, True),
 (_TEXT_REGEXP, False)
) #: In order of expected frequency. A value of True means that the server needs to process the line instead of just forwarding it.

class Lobby(threading.Thread):
	"""
	A class for managing the lobby component of a UTc! server.
	"""
	_alive = True #: False when the server has died.
	_client_lock = None #: A lock used to facilitate communication between clients and lobby-space.
	_game_port = None #: Non-None when a game is in progress.
	_play_callback = None #: The function used to add a player to a game.
	_players = None #: A map of all players that are connected to the lobby.
	_player_client_map = None #: The mapping between player names and connections.
	_server = None #: The network server associated with this lobby.
	_teams = None #: A collection of all teams.
	_validate_callback = None #: A function that may be used to determine whether the player's name is in use.
	
	def __init__(self, port, play_callback, validate_callback):
		"""
		Sets up and starts the lobby server.
		
		@type port: int
		@param port: The port on which the lobby server will run.
		@type play_callback: callable
		@param play_callback: The function to call when a client wants to join an
		    in-progress game.
		@type validate_callback: callable
		@param validate_callback: The function to call when deciding whether a
		    player's name is already in use.
		
		@raise socket.error: If the specified port is already in use.
		"""
		threading.Thread.__init__(self)
		self.setDaemon(False)
		
		self._client_lock = threading.RLock()
		self._player_client_map = {}
		self._players = {}
		self._teams = tuple([_Team() for i in range(6)])
		
		self._play_callback = play_callback
		self._validate_callback = validate_callback
		
		self._server = LobbyServer(port, self)
		self._server.start()
		
	def addPlayer(self, player_name):
		"""
		Adds a player to the lobby, creating an _Player object to maintain a local
		representation of its state.
		
		@type player_name: basestring
		@param player_name: The name of the player to be added.
		
		@rtype: L{_Player}
		@return: The newly created representative player object.
		"""
		self._client_lock.acquire()
		try:
			target_team = self._teams[0]
			target_team_size = len(target_team.getMembers())
			target_team_id = 0
			for (i, team) in enumerate(self._teams[1:]):
				team_size = len(team.getMembers())
				if team_size < target_team_size:
					target_team = team
					target_team_size = team_size
					target_team_id = i + 1
					
			player = _Player(player_name, target_team_id)
			self._players[player_name] = player
			target_team.addMember(player)
			
			self._pushEvent("JOIN %s %i" % (player_name, target_team_id + 1))
			
			return player
		finally:
			self._client_lock.release()
			
	def connectPlayer(self, player, connection):
		"""
		Handles the process of completing a connection from a client to the lobby
		server.
		
		@type player: L{_Player}
		@param player: The player being connected.
		@type connection: L{ClientConnection}
		@param connection: The connection thread that manages communication with
		    this player.
		"""
		self._client_lock.acquire()
		try:
			player_name = player.getName()
			self._player_client_map[player_name] = connection
			if self._game_port:
				self._pushEvent("GSTR", player_name, False)
		finally:
			self._client_lock.release()
			
	def disconnectPlayer(self, player_name):
		"""
		Removes a player from this lobby server.
		
		@type player_name: basestring
		@param player_name: The name of the player who is being removed.
		"""
		self._client_lock.acquire()
		try:
			if self._dropPlayer(player_name):
				self._pushEvent("PART %s" % (player_name))
		finally:
			self._client_lock.release()
			
	def endGame(self, teams, players):
		"""
		Called to inform all clients that the game server has shut down.
		
		All team stats and player stats are published by this function.
		
		@type teams: sequence
		@param teams: A collection of L{game._Team} objects.
		@type players: sequence
		@param players: A collection of L{game._Player} objects.
		"""
		self._game_port = None
		self._pushEvent("GEND")
		for (i, team) in enumerate(teams):
			self._pushEvent("TSTT %i %i %i" % (i + 1, team.getRank(), team.getScore()))
		for player in reversed(sorted(players)):
			(accuracy, kills, deaths, suicides) = player.getStats()
			self._pushEvent("PSTT %s %i %i %i %i %s" % (player.getName(), player.getTeamID() + 1, kills, deaths, suicides, accuracy))
			
	def getPlayers(self):
		"""
		Returns a collection of all players who are connected to this lobby
		server.
		
		@rtype: sequence
		@return: A collection of L{_Player} objects.
		"""
		self._client_lock.acquire()
		try:
			return self._players.values()
		finally:
			self._client_lock.release()
			
	def getTeam(self, id):
		"""
		Returns a team-status representative container.
		
		@type id: int
		@param id: The ID of the team to retrieve.
		
		@rtype: L{_Team}
		@return: The team being requested.
		"""
		return self._teams[id]
		
	def hasPlayer(self, name):
		"""
		Indicates whether this lobby server has a player with the given name.
		
		@type name: basestring
		@param name: The name of the user being sought.
		
		@rtype: bool
		@return: True if the named user is present.
		"""
		self._client_lock.acquire()
		try:
			return name in self._players
		finally:
			self._client_lock.release()
			
	def isAlive(self):
		"""
		Indicates whether this lobby server is active.
		
		@rtype: bool
		@return: True if this lobby server is still active.
		"""
		return self._alive
		
	def kill(self):
		"""
		Indicates that this lobby server should terminate gracefully.
		"""
		self._alive = False
		
	def processLine(self, line, player_name):
		"""
		Interprets data from a client, applying protocol regexps to determine how
		to react.
		
		@type line: basestring
		@param line: The line to process.
		@type player_name: basestring
		@param player_name: The name of the player who generated this line.
		"""
		for (regexp, process) in _REGEXP_HANDLING:
			match = regexp.match(line)
			if match:
				if not process: #Just forward it.
					self._pushEvent(line, player_name, True)
					break
				else: #Determine the nature of the event and deal with it.
					nature = match.group(1)
					self._client_lock.acquire()
					try:
						if nature == "TEAM":
							player = self._players.get(player_name)
							if player:
								team_id = int(match.group(2)) - 1
								if team_id < len(self._teams):
									team = self._teams[team_id]
									if len(team.getMembers()) < _TEAM_SIZE:
										self._teams[player.getTeamID()].removeMember(player)
										self._teams[team_id].addMember(player) 
										player.setTeamID(team_id)
										self._pushEvent("TEAM %s %i" % (player_name, team_id + 1))
										return
								self._pushEvent("TEAM %s %i" % (player_name, player.getTeamID() + 1), player_name, False)
						elif nature == "PART":
							for (name, connection) in self._player_client_map.iteritems():
								if name == player_name:
									connection.kill()
									return
							self._pushEvent("PART %s" % (player_name), player_name, True)
						elif nature == "PLAY":
							player = self._players.get(player_name)
							if player:
								if self._game_port:
									join_result = self._play_callback(
									 player_name, player.getTeamID(),
									 (float(match.group(2)) / 1000.0, float(match.group(3)) / 1000.0, float(match.group(4)) / 1000.0),
									 match.group(5),
									 (match.group(6), match.group(7), match.group(8), match.group(9)),
									 (int(match.group(10)), int(match.group(11)), int(match.group(12)), int(match.group(13))),
									 (match.group(14), match.group(15), match.group(16)),
									 int(match.group(17))
									)
									if not join_result:
										self._pushEvent("GAME %i" % (self._game_port), player_name, False)
									else:
										self._pushEvent("NOGM %s" % (join_result), player_name, False)	
								else:
									self._pushEvent("NOGM no game in progress", player_name, False)
					finally:
						self._client_lock.release()
					break
					
	def run(self):
		"""
		Keeps this lobby server running as long as the host maintains the game
		process.
		"""
		while self._alive:
			time.sleep(10.0)
			
		self._server.kill()
		self._pushEvent("SEND")
		for connection in self._player_client_map.values():
			connection.kill()
			
	def startGame(self, port):
		"""
		Informs all clients that a game server has started.
		"""
		self._game_port = port
		self._pushEvent("GSTR")
		
	def validatePlayer(self, player_name):
		"""
		Determines whether the named player is known to either the lobby server
		or the game server, if any.
		
		@type player_name: basestring
		@param player_name: The name of the player being sought.
		
		@rtype: str|None
		@return: A string indicating where the player was found or None if the
		    player was not found.
		"""
		if self.hasPlayer(player_name):
			return "player already in lobby"
		elif self._validate_callback(player_name):
			return "player currently in game"
		return None
		
	def _dropPlayer(self, player_name):
		"""
		Performs the practical process of removing a player from this lobby
		server, if the player was actually present to begin with.
		
		@type player_name: basestring
		@param player_name: The name of the player to be removed.
		
		@rtype: bool
		@return: True if the player was removed; False if the player was not
		    found.
		"""
		self._client_lock.acquire()
		try:
			player = self._players.get(player_name)
			if player:
				del self._players[player_name]
				self._teams[player.getTeamID()].removeMember(player)
				return True
			return False
		finally:
			self._client_lock.release()
			
	def _pushEvent(self, event, player_filter=None, exclude_named_player=False):
		"""
		Propagates an event to a subset of all connected clients.
		
		@type event: basestring
		@param event: The event to send.
		@type player_filter: basestring|None
		@param player_filter: The name of the player to treat specially, or None
		    if all players should receive this message.
		@type exclude_named_player: bool
		@param exclude_named_player: True if the named player should not receive
		    this event; False if B{only} the named player should receive it.
		"""
		self._client_lock.acquire()
		try:
			if not player_filter:
				for (name, connection) in self._player_client_map.iteritems():
					connection.pushEvent(event)
			else:
				for (name, connection) in self._player_client_map.iteritems():
					if exclude_named_player:
						if name == player_filter:
							continue
						connection.pushEvent(event)
					elif not exclude_named_player and name == player_filter:
						connection.pushEvent(event)
						break
		finally:
			self._client_lock.release()
			
			
class LobbyServer(communication.ServerSocket):
	"""
	A server process that continually listens for and accepts connections from
	would-be clients.
	"""
	def __init__(self, port, lobby):
		communication.ServerSocket.__init__(self, port, ClientConnection, (lobby,))
		
		
class ClientConnection(threading.Thread):
	"""
	A connection from a client.
	"""
	_alive = True #: False when this thread is ready to be killed.
	_client_socket = None #: The connection to the client.
	_data_box = None #: A container for data remaining to be processed.
	_lobby = None #: The lobby with which the client interacts.
	_lock = None #: A lock to keep the event queue thread-safe.
	_queue = None #: A list of messages to send to the player.
	_player = None #: The _Player object of the player this thread manages.
	_player_name = None #: The name of the player this connection handles.
	
	def __init__(self, client, args):
		"""
		Performs a handshake with the would-be client before accepting the
		connection and exposing it to the server.
		
		@type client: C{socket.socket}
		@param client: The socket by which connection with the client is made
		    possible.
		@type args: sequence
		@param args: The lobby server with which this client will interact.
		"""
		threading.Thread.__init__(self)
		self.setDaemon(False)
		
		(lobby,) = args
		
		self._lock = threading.RLock()
		self._queue = []
		self._lobby = lobby
		self._data_box = data_box.DataBox()
		self._client_socket = client_socket = communication.ClientSocket(client, self._data_box)
		
		try:
			self._client_socket.readData(True)
		except: #No harm in failing at this point.
			self.kill()
			return
		data = self._data_box.getLine()
		try:
			match = re.match(r"(\w+) (\d+\.\d+(?:\.\d+)?[a-z]?)", data)
			if not match:
				self._client_socket.sendData("FAIL invalid identifier")
				self.kill()
			elif not match.group(2) == _VERSION:
				self._client_socket.sendData("FAIL server is running version %s" % _VERSION)
				self.kill()
			elif not lobby.isAlive():
				self._client_socket.sendData("FAIL server has shut down")
				self.kill()
			else:
				player_name = match.group(1)
				error = lobby.validatePlayer(player_name)
				if error:
					self._client_socket.sendData("FAIL %s" % (error))
					self.kill()
				else:
					self._player = lobby.addPlayer(player_name)
					self._player_name = player_name
					
					self._client_socket.sendData("PASS %s %i" % (player_name, self._player.getTeamID() + 1))
					for description in [other_player.describe() for other_player in self._lobby.getPlayers() if not other_player == self._player]:
						self._client_socket.sendData(description)
					self._client_socket.sendData("LEND")
					
					lobby.connectPlayer(self._player, self)
		except Exception, e:
			print e
			lobby.disconnectPlayer(self._player_name)
			
	def kill(self):
		"""
		Indicates that this connection should terminate gracefully.
		"""
		self._alive = False
		
	def pushEvent(self, event):
		"""
		Queues an event for delivery to the client.
		
		@type event: basestring
		@param event: A non-newline-terminated protocol event.
		"""
		self._lock.acquire()
		self._queue.append(event)
		self._lock.release()
		
	def run(self):
		"""
		Continually reads and processes data from the client, and delivers any
		queued data.
		"""
		readData = self._client_socket.readData
		sendData = self._client_socket.sendData
		data_box = self._data_box
		lock = self._lock
		queue = self._queue
		lobby = self._lobby
		player_name = self._player_name
		try:
			while self._alive:
				readData()
				while data_box.hasLines():
					lobby.processLine(data_box.getLine(), player_name)
					
				lock.acquire()
				if queue:
					sendData('\n'.join(queue))
					self._queue = queue = []
				lock.release()
				
				time.sleep(0.25)
		except:
			pass
		lobby.disconnectPlayer(player_name)
		self._client_socket.kill()
		
		
class _Player(object):
	"""
	A container that locally represents a player connected to a lobby server.
	"""
	_name = None #: The player's name.
	_team_id = None #: The ID of the team to which this player belongs.
	
	def __init__(self, name, team_id):
		"""
		Initialises the state of this player, setting the name and team to which
		it belongs.
		
		@type name: basestring
		@param name: The name of the player being represented.
		@type team_id: int
		@param team_id: The ID of the team to which this player initially belongs.
		"""
		self._name = name
		self._team_id = team_id
		
	def describe(self):
		"""
		Returns a protocol-event description of this player.
		
		@rtype: str
		@return: A protocol-event description of this player.
		"""
		return "PLYR %s %i" % (self._name, self._team_id + 1)
		
	def getName(self):
		"""
		Returns the name of this player.
		
		@rtype: str
		@return: The name of this player.
		"""
		return self._name
		
	def getTeamID(self):
		"""
		Returns the team ID of this player.
		
		@rtype: int
		@return: This player's team ID.
		"""
		return self._team_id
		
	def setTeamID(self, team_id):
		"""
		Assigns a new team ID to this player.
		
		@type team_id: int
		@param team_id: The team ID to assign to this player.
		"""
		self._team_id = team_id
		
		
class _Team(object):
	"""
	A container that locally represents the membership of a team.
	"""
	_lock = None #: A lock used for thread control.
	_members = None #: The players who comprise this team.
	
	def __init__(self):
		"""
		Initialises this team.
		"""
		self._members = []
		self._lock = threading.Lock()
		
	def addMember(self, player):
		"""
		Adds a player to this team.
		
		@type player: L{_Player}
		@param player: The player to be added.
		"""
		self._lock.acquire()
		if not player in self._members:
			self._members.append(player)
		self._lock.release()
		
	def getMembers(self):
		"""
		Returns a list of all players that comprise this team.
		
		@rtype: list
		@return: A list of all L{_Player}s that comprise this team.
		"""
		self._lock.acquire()
		try:
			return self._members[:]
		finally:
			self._lock.release()
			
	def removeMember(self, player):
		"""
		Removes a player from this team.
		
		@type player: L{_Player}
		@param player: The player to be removed.
		"""
		self._lock.acquire()
		if player in self._members:
			self._members.remove(player)
		self._lock.release()
		